/*
The MIT License (MIT)

Copyright (c) 2013 Sergey Nechaev <serg.nechaev@gmail.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
 */
package com.sergnechaev.web.combine.filter;

import java.io.IOException;
import java.io.InputStream;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.SystemUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.joda.time.DateTime;
import org.joda.time.Seconds;

import com.sergnechaev.web.combine.filter.helper.Minifier;
import com.sergnechaev.web.combine.filter.helper.UrlHelper;

/**
 * 
 * @author Sergey Nechaev <serg.nechaev@gmail.com>
 * 
 */
public class CombineAndMinifyFilter implements Filter {

	private static final Log log = LogFactory.getLog(CombineAndMinifyFilter.class);

	private String APP_VERSION_SUFFIX = "-myapp";

	private static final Map<String, Resource> RESOURCES = new HashMap<String, Resource>();

	private static final Minifier MINIFIER = new Minifier();

	private FilterConfig _filterConfig;

	private boolean cacheWebResources = true;

	public void destroy() {
		log.debug(this.getClass().getName() + " destroyed");
	}

	public void clearCache() {
		RESOURCES.clear();
		log.warn("The cache was cleared!");
	}

	public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {

		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) resp;

		// catch 'pass filter' scenario
		if (StringUtils.isBlank(request.getParameter("passFilter"))) {

			final String url = UrlHelper.clean(request.getRequestURL().toString());
			final String uri = UrlHelper.clean(request.getRequestURI());

			try {

				log.debug("Resource URL: " + url);
				log.debug("Resource URI: " + uri);

				// check for cached resource
				if (RESOURCES.containsKey(uri) && cacheWebResources) {

					log.debug("URL from cache: " + uri);

					final Resource r = RESOURCES.get(uri);

					final String eTag = request.getHeader("If-None-Match");

					if (r.eTag.equals(eTag)) {
						r.writeNotModified(response);
					} else {
						r.writeOk(response);
					}

					return;
				}

				// String fileName =
				// (url.substring(url.indexOf("/resources/")+"/resources/".length()));
				final String fileName = (url.substring(url.indexOf("/resources/") + "/resources/".length(), url.lastIndexOf(".")));
				final String extension = FilenameUtils.getExtension(url);
				final String path = url.substring(0, url.indexOf("/resources/")) + "/static/";

				if (log.isDebugEnabled()) {
					log.debug("Resource File name: " + fileName);
					log.debug("Resource Extension: " + extension);
					log.debug("Resource Path: " + path);
				}

				if (fileName.endsWith(APP_VERSION_SUFFIX)) {

					final String realScriptBaseName = fileName.substring(0, fileName.lastIndexOf(APP_VERSION_SUFFIX));

					final String[] realScriptNames = realScriptBaseName.split("&");

					final StringBuilder resource = new StringBuilder();

					for (final String name : realScriptNames) {

						log.debug("Real Resource Name: " + name);

						// retrieve scripts:
						String u = name + "." + extension;
						log.debug("Real Resource path: " + u);

						InputStream is = null;

						try {
							// is =
							// this.getClass().getResourceAsStream("/com/qaxml/web/resources/"
							// + u);

							is = this._filterConfig.getServletContext().getResourceAsStream("static/" + u);

							String content = IOUtils.toString(is, "UTF-8");

							content = minify(name, extension, content);

							resource.append(SystemUtils.LINE_SEPARATOR + "/*! " + SystemUtils.LINE_SEPARATOR + " * " + name + "." + extension + SystemUtils.LINE_SEPARATOR + " */ "
									+ SystemUtils.LINE_SEPARATOR);

							resource.append(content).append(SystemUtils.LINE_SEPARATOR);

						} catch (Exception e) {
							log.error("Error: " + u, e);
						} finally {
							IOUtils.closeQuietly(is);
						}

					}

					// dump to response stream
					final Resource r = createResource(uri, fileName, extension, resource.toString());

					r.writeOk(response);

					return;

				}

			} catch (Exception e) {
				log.error("Error: " + url, e);
			}
		} else {
			log.debug("Passing cache for " + request.getRequestURI());
		}

		chain.doFilter(req, resp);

	}

	public void init(FilterConfig filterConfig) throws ServletException {

		_filterConfig = filterConfig;

		log.debug(this.getClass().getName() + " started");
	}

	private String minify(String fileName, String type, String data) throws Exception {
		if (false == fileName.contains(".min")) {
			if ("css".equals(type)) {
				return MINIFIER.minifyCSS(data);
			} else if ("js".equals(type)) {
				return MINIFIER.minifyJS(data);
			}
		}
		return data;
	}

	private Resource createResource(String uri, String fileName, String type, String data) throws Exception {

		// minify all not .min.
		// Set up content type

		int length = data.getBytes("UTF-8").length;

		Date now = new Date();
		Calendar c = Calendar.getInstance();
		c.setTime(now);

		// Last-Modified header (1 year ago)
		c.set(Calendar.YEAR, Calendar.getInstance().get(Calendar.YEAR) - 1);
		long lastModified = c.getTimeInMillis();

		// Set 'forever cached' policy
		// Expires header (in 1 year)
		c.set(Calendar.YEAR, Calendar.getInstance().get(Calendar.YEAR) + 1);
		long expires = c.getTimeInMillis();

		// ETag headers
		String eTag = DigestUtils.md5Hex(data);

		// Cache-Control header
		DateTime startTime = new DateTime(now.getTime());
		DateTime endTime = new DateTime(c.getTimeInMillis());
		Seconds seconds = Seconds.secondsBetween(startTime, endTime);

		String cacheControl = "public, max-age=" + seconds.getSeconds() + ", must-revalidate";

		// Set up content type
		String contentType = null;
		if ("css".equals(type)) {
			contentType = "text/css";
		} else if ("js".equals(type)) {
			contentType = "text/javascript";
		}

		// create resource
		Resource r = new Resource();
		r.cacheControl = cacheControl;
		r.contentType = contentType;
		r.data = data;
		r.eTag = eTag;
		r.expires = expires;
		r.lastModified = lastModified;
		r.length = length;

		RESOURCES.put(uri, r);

		return r;
	}
}
